#pragma once

#include "../widgets.h"
#include "basechart_axis.h"
#include "basechart_datacontainer.h"
#include "basechart_layer.h"
#include "basechart_plot.h"
#include "basechart_selection.h"

class BaseChartPlottableInterface1D {
 public:
  virtual ~BaseChartPlottableInterface1D() = default;
  // introduced pure virtual methods:
  virtual int dataCount() const = 0;
  virtual double dataMainKey(int index) const = 0;
  virtual double dataSortKey(int index) const = 0;
  virtual double dataMainValue(int index) const = 0;
  virtual BaseChartRange dataValueRange(int index) const = 0;
  virtual QPointF dataPixelPosition(int index) const = 0;
  virtual bool sortKeyIsMainKey() const = 0;
  virtual BaseChartDataSelection selectTestRect(const QRectF& rect,
                                                bool onlySelectable) const = 0;
  virtual int findBegin(double sortKey, bool expandedRange = true) const = 0;
  virtual int findEnd(double sortKey, bool expandedRange = true) const = 0;
};

class BaseChartAxis;
class BaseChartLegend;
class S_WIDGETS_EXPORT BaseChartAbstractPlottable : public BaseChartLayerable {
  Q_OBJECT
  /// \cond INCLUDE_QPROPERTIES
  Q_PROPERTY(QString name READ name WRITE setName)
  Q_PROPERTY(bool antialiasedFill READ antialiasedFill WRITE setAntialiasedFill)
  Q_PROPERTY(bool antialiasedScatters READ antialiasedScatters WRITE
                 setAntialiasedScatters)
  Q_PROPERTY(QPen pen READ pen WRITE setPen)
  Q_PROPERTY(QBrush brush READ brush WRITE setBrush)
  Q_PROPERTY(BaseChartAxis* keyAxis READ keyAxis WRITE setKeyAxis)
  Q_PROPERTY(BaseChartAxis* valueAxis READ valueAxis WRITE setValueAxis)
  Q_PROPERTY(SelectionType selectable READ selectable WRITE setSelectable NOTIFY
                 selectableChanged)
  Q_PROPERTY(BaseChartDataSelection selection READ selection WRITE setSelection
                 NOTIFY selectionChanged)
  Q_PROPERTY(BaseChartSelectionDecorator* selectionDecorator READ
                 selectionDecorator WRITE setSelectionDecorator)
  /// \endcond
 public:
  BaseChartAbstractPlottable(BaseChartAxis* keyAxis, BaseChartAxis* valueAxis);
  virtual ~BaseChartAbstractPlottable() Q_DECL_OVERRIDE;

  // getters:
  QString name() const { return mName; }
  bool antialiasedFill() const { return mAntialiasedFill; }
  bool antialiasedScatters() const { return mAntialiasedScatters; }
  QPen pen() const { return mPen; }
  QBrush brush() const { return mBrush; }
  BaseChartAxis* keyAxis() const { return mKeyAxis.data(); }
  BaseChartAxis* valueAxis() const { return mValueAxis.data(); }
  SelectionType selectable() const { return mSelectable; }
  bool selected() const { return !mSelection.isEmpty(); }
  BaseChartDataSelection selection() const { return mSelection; }
  BaseChartSelectionDecorator* selectionDecorator() const {
    return mSelectionDecorator;
  }

  // setters:
  void setName(const QString& name);
  void setAntialiasedFill(bool enabled);
  void setAntialiasedScatters(bool enabled);
  void setPen(const QPen& pen);
  void setBrush(const QBrush& brush);
  void setKeyAxis(BaseChartAxis* axis);
  void setValueAxis(BaseChartAxis* axis);
  Q_SLOT void setSelectable(SelectionType selectable);
  Q_SLOT void setSelection(BaseChartDataSelection selection);
  void setSelectionDecorator(BaseChartSelectionDecorator* decorator);

  // introduced virtual methods:
  virtual double selectTest(const QPointF& pos, bool onlySelectable,
                            QVariant* details = nullptr) const
      Q_DECL_OVERRIDE = 0;  // actually introduced in QCPLayerable as non-pure,
                            // but we want to force reimplementation for
                            // plottables
  virtual BaseChartPlottableInterface1D* interface1D() { return nullptr; }
  virtual BaseChartRange getKeyRange(
      bool& foundRange, SignDomain inSignDomain = sdBoth) const = 0;
  virtual BaseChartRange getValueRange(
      bool& foundRange, SignDomain inSignDomain = sdBoth,
      const BaseChartRange& inKeyRange = BaseChartRange()) const = 0;

  // non-property methods:
  void coordsToPixels(double key, double value, double& x, double& y) const;
  const QPointF coordsToPixels(double key, double value) const;
  void pixelsToCoords(double x, double y, double& key, double& value) const;
  void pixelsToCoords(const QPointF& pixelPos, double& key,
                      double& value) const;
  void rescaleAxes(bool onlyEnlarge = false) const;
  void rescaleKeyAxis(bool onlyEnlarge = false) const;
  void rescaleValueAxis(bool onlyEnlarge = false,
                        bool inKeyRange = false) const;
  bool addToLegend(BaseChartLegend* legend);
  bool addToLegend();
  bool removeFromLegend(BaseChartLegend* legend) const;
  bool removeFromLegend() const;

 signals:
  void selectionChanged(bool selected);
  void selectionChanged(const BaseChartDataSelection& selection);
  void selectableChanged(SelectionType selectable);

 protected:
  // property members:
  QString mName;
  bool mAntialiasedFill, mAntialiasedScatters;
  QPen mPen;
  QBrush mBrush;
  QPointer<BaseChartAxis> mKeyAxis, mValueAxis;
  SelectionType mSelectable;
  BaseChartDataSelection mSelection;
  BaseChartSelectionDecorator* mSelectionDecorator;

  // reimplemented virtual methods:
  virtual QRect clipRect() const Q_DECL_OVERRIDE;
  virtual void draw(BaseChartPainter* painter) Q_DECL_OVERRIDE = 0;
  virtual Interaction selectionCategory() const Q_DECL_OVERRIDE;
  void applyDefaultAntialiasingHint(BaseChartPainter* painter) const
      Q_DECL_OVERRIDE;
  // events:
  virtual void selectEvent(QMouseEvent* event, bool additive,
                           const QVariant& details,
                           bool* selectionStateChanged) Q_DECL_OVERRIDE;
  virtual void deselectEvent(bool* selectionStateChanged) Q_DECL_OVERRIDE;

  // introduced virtual methods:
  virtual void drawLegendIcon(BaseChartPainter* painter,
                              const QRectF& rect) const = 0;

  // non-virtual methods:
  void applyFillAntialiasingHint(BaseChartPainter* painter) const;
  void applyScattersAntialiasingHint(BaseChartPainter* painter) const;

 private:
  Q_DISABLE_COPY(BaseChartAbstractPlottable)

  friend class BaseChartCustomPlot;
  friend class BaseChartAxis;
  friend class BaseChartPlottableLegendItem;
};

template <class DataType>
class BaseChartAbstractPlottable1D
    : public BaseChartAbstractPlottable,
      public BaseChartPlottableInterface1D  // no QCP_LIB_DECL, template class
                                            // ends up in header (cpp included
                                            // below)
{
  // No Q_OBJECT macro due to template class

 public:
  BaseChartAbstractPlottable1D(BaseChartAxis* keyAxis,
                               BaseChartAxis* valueAxis);
  virtual ~BaseChartAbstractPlottable1D() Q_DECL_OVERRIDE;

  // virtual methods of 1d plottable interface:
  virtual int dataCount() const Q_DECL_OVERRIDE;
  virtual double dataMainKey(int index) const Q_DECL_OVERRIDE;
  virtual double dataSortKey(int index) const Q_DECL_OVERRIDE;
  virtual double dataMainValue(int index) const Q_DECL_OVERRIDE;
  virtual BaseChartRange dataValueRange(int index) const Q_DECL_OVERRIDE;
  virtual QPointF dataPixelPosition(int index) const Q_DECL_OVERRIDE;
  virtual bool sortKeyIsMainKey() const Q_DECL_OVERRIDE;
  virtual BaseChartDataSelection selectTestRect(
      const QRectF& rect, bool onlySelectable) const Q_DECL_OVERRIDE;
  virtual int findBegin(double sortKey,
                        bool expandedRange = true) const Q_DECL_OVERRIDE;
  virtual int findEnd(double sortKey,
                      bool expandedRange = true) const Q_DECL_OVERRIDE;

  // reimplemented virtual methods:
  virtual double selectTest(const QPointF& pos, bool onlySelectable,
                            QVariant* details = nullptr) const Q_DECL_OVERRIDE;
  virtual BaseChartPlottableInterface1D* interface1D() Q_DECL_OVERRIDE {
    return this;
  }

 protected:
  // property members:
  QSharedPointer<BaseChartDataContainer<DataType> > mDataContainer;

  // helpers for subclasses:
  void getDataSegments(QList<BaseChartDataRange>& selectedSegments,
                       QList<BaseChartDataRange>& unselectedSegments) const;
  void drawPolyline(BaseChartPainter* painter,
                    const QVector<QPointF>& lineData) const;

 private:
  Q_DISABLE_COPY(BaseChartAbstractPlottable1D)
};

////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////// BaseChartAbstractPlottable1D
////////////////////////////////////////////////////////////////////////////////////////////////////

/*! \class BaseChartAbstractPlottable1D
  \brief A template base class for plottables with one-dimensional data

  This template class derives from \ref BaseChartAbstractPlottable and from the
  abstract interface \ref QCPPlottableInterface1D. It serves as a base class for
  all one-dimensional data (i.e. data with one key dimension), such as \ref
  QCPGraph and QCPCurve.

  The template parameter \a DataType is the type of the data points of this
  plottable (e.g. \ref QCPGraphData or \ref QCPCurveData). The main purpose of
  this base class is to provide the member \a mDataContainer (a shared pointer
  to a \ref BaseChartDataContainer "BaseChartDataContainer<DataType>") and
  implement the according virtual methods of the \ref QCPPlottableInterface1D,
  such that most subclassed plottables don't need to worry about this anymore.

  Further, it provides a convenience method for retrieving selected/unselected
  data segments via \ref getDataSegments. This is useful when subclasses
  implement their \ref draw method and need to draw selected segments with a
  different pen/brush than unselected segments (also see \ref
  QCPSelectionDecorator).

  This class implements basic functionality of \ref
  BaseChartAbstractPlottable::selectTest and \ref
  QCPPlottableInterface1D::selectTestRect, assuming point-like data points,
  based on the 1D data interface. In spite of that, most plottable subclasses
  will want to reimplement those methods again, to provide a more accurate hit
  test based on their specific data visualization geometry.
*/

/* start documentation of inline functions */

/*! \fn QCPPlottableInterface1D *BaseChartAbstractPlottable1D::interface1D()

  Returns a \ref QCPPlottableInterface1D pointer to this plottable, providing
  access to its 1D interface.

  \seebaseclassmethod
*/

/* end documentation of inline functions */

/*!
  Forwards \a keyAxis and \a valueAxis to the \ref
  BaseChartAbstractPlottable::BaseChartAbstractPlottable
  "BaseChartAbstractPlottable" constructor and allocates the \a mDataContainer.
*/
template <class DataType>
BaseChartAbstractPlottable1D<DataType>::BaseChartAbstractPlottable1D(
    BaseChartAxis* keyAxis, BaseChartAxis* valueAxis)
    : BaseChartAbstractPlottable(keyAxis, valueAxis),
      mDataContainer(new BaseChartDataContainer<DataType>) {}

template <class DataType>
BaseChartAbstractPlottable1D<DataType>::~BaseChartAbstractPlottable1D() {}

/*!
  \copydoc QCPPlottableInterface1D::dataCount
*/
template <class DataType>
int BaseChartAbstractPlottable1D<DataType>::dataCount() const {
  return mDataContainer->size();
}

/*!
  \copydoc QCPPlottableInterface1D::dataMainKey
*/
template <class DataType>
double BaseChartAbstractPlottable1D<DataType>::dataMainKey(int index) const {
  if (index >= 0 && index < mDataContainer->size()) {
    return (mDataContainer->constBegin() + index)->mainKey();
  } else {
    qDebug() << Q_FUNC_INFO << "Index out of bounds" << index;
    return 0;
  }
}

/*!
  \copydoc QCPPlottableInterface1D::dataSortKey
*/
template <class DataType>
double BaseChartAbstractPlottable1D<DataType>::dataSortKey(int index) const {
  if (index >= 0 && index < mDataContainer->size()) {
    return (mDataContainer->constBegin() + index)->sortKey();
  } else {
    qDebug() << Q_FUNC_INFO << "Index out of bounds" << index;
    return 0;
  }
}

/*!
  \copydoc QCPPlottableInterface1D::dataMainValue
*/
template <class DataType>
double BaseChartAbstractPlottable1D<DataType>::dataMainValue(int index) const {
  if (index >= 0 && index < mDataContainer->size()) {
    return (mDataContainer->constBegin() + index)->mainValue();
  } else {
    qDebug() << Q_FUNC_INFO << "Index out of bounds" << index;
    return 0;
  }
}

/*!
  \copydoc QCPPlottableInterface1D::dataValueRange
*/
template <class DataType>
BaseChartRange BaseChartAbstractPlottable1D<DataType>::dataValueRange(
    int index) const {
  if (index >= 0 && index < mDataContainer->size()) {
    return (mDataContainer->constBegin() + index)->valueRange();
  } else {
    qDebug() << Q_FUNC_INFO << "Index out of bounds" << index;
    return BaseChartRange(0, 0);
  }
}

/*!
  \copydoc QCPPlottableInterface1D::dataPixelPosition
*/
template <class DataType>
QPointF BaseChartAbstractPlottable1D<DataType>::dataPixelPosition(
    int index) const {
  if (index >= 0 && index < mDataContainer->size()) {
    const typename BaseChartDataContainer<DataType>::const_iterator it =
        mDataContainer->constBegin() + index;
    return coordsToPixels(it->mainKey(), it->mainValue());
  } else {
    qDebug() << Q_FUNC_INFO << "Index out of bounds" << index;
    return QPointF();
  }
}

/*!
  \copydoc QCPPlottableInterface1D::sortKeyIsMainKey
*/
template <class DataType>
bool BaseChartAbstractPlottable1D<DataType>::sortKeyIsMainKey() const {
  return DataType::sortKeyIsMainKey();
}

/*!
  Implements a rect-selection algorithm assuming the data (accessed via the 1D
  data interface) is point-like. Most subclasses will want to reimplement this
  method again, to provide a more accurate hit test based on the true data
  visualization geometry.

  \seebaseclassmethod
*/
template <class DataType>
BaseChartDataSelection BaseChartAbstractPlottable1D<DataType>::selectTestRect(
    const QRectF& rect, bool onlySelectable) const {
  BaseChartDataSelection result;
  if ((onlySelectable && mSelectable == stNone) || mDataContainer->isEmpty())
    return result;
  if (!mKeyAxis || !mValueAxis) return result;

  // convert rect given in pixels to ranges given in plot coordinates:
  double key1, value1, key2, value2;
  pixelsToCoords(rect.topLeft(), key1, value1);
  pixelsToCoords(rect.bottomRight(), key2, value2);
  BaseChartRange keyRange(
      key1, key2);  // BaseChartRange normalizes internally so we don't have to
                    // care about whether key1 < key2
  BaseChartRange valueRange(value1, value2);
  typename BaseChartDataContainer<DataType>::const_iterator begin =
      mDataContainer->constBegin();
  typename BaseChartDataContainer<DataType>::const_iterator end =
      mDataContainer->constEnd();
  if (DataType::sortKeyIsMainKey())  // we can assume that data is sorted by
                                     // main key, so can reduce the searched key
                                     // interval:
  {
    begin = mDataContainer->findBegin(keyRange.lower, false);
    end = mDataContainer->findEnd(keyRange.upper, false);
  }
  if (begin == end) return result;

  int currentSegmentBegin =
      -1;  // -1 means we're currently not in a segment that's contained in rect
  for (typename BaseChartDataContainer<DataType>::const_iterator it = begin;
       it != end; ++it) {
    if (currentSegmentBegin == -1) {
      if (valueRange.contains(it->mainValue()) &&
          keyRange.contains(it->mainKey()))  // start segment
        currentSegmentBegin = int(it - mDataContainer->constBegin());
    } else if (!valueRange.contains(it->mainValue()) ||
               !keyRange.contains(it->mainKey()))  // segment just ended
    {
      result.addDataRange(
          BaseChartDataRange(currentSegmentBegin,
                             int(it - mDataContainer->constBegin())),
          false);
      currentSegmentBegin = -1;
    }
  }
  // process potential last segment:
  if (currentSegmentBegin != -1)
    result.addDataRange(
        BaseChartDataRange(currentSegmentBegin,
                           int(end - mDataContainer->constBegin())),
        false);

  result.simplify();
  return result;
}

/*!
  \copydoc QCPPlottableInterface1D::findBegin
*/
template <class DataType>
int BaseChartAbstractPlottable1D<DataType>::findBegin(
    double sortKey, bool expandedRange) const {
  return int(mDataContainer->findBegin(sortKey, expandedRange) -
             mDataContainer->constBegin());
}

/*!
  \copydoc QCPPlottableInterface1D::findEnd
*/
template <class DataType>
int BaseChartAbstractPlottable1D<DataType>::findEnd(double sortKey,
                                                    bool expandedRange) const {
  return int(mDataContainer->findEnd(sortKey, expandedRange) -
             mDataContainer->constBegin());
}

/*!
  Implements a point-selection algorithm assuming the data (accessed via the 1D
  data interface) is point-like. Most subclasses will want to reimplement this
  method again, to provide a more accurate hit test based on the true data
  visualization geometry.

  If \a details is not 0, it will be set to a \ref BaseChartDataSelection,
  describing the closest data point to \a pos.

  \seebaseclassmethod
*/
template <class DataType>
double BaseChartAbstractPlottable1D<DataType>::selectTest(
    const QPointF& pos, bool onlySelectable, QVariant* details) const {
  if ((onlySelectable && mSelectable == stNone) || mDataContainer->isEmpty())
    return -1;
  if (!mKeyAxis || !mValueAxis) return -1;

  BaseChartDataSelection selectionResult;
  double minDistSqr = (std::numeric_limits<double>::max)();
  int minDistIndex = mDataContainer->size();

  typename BaseChartDataContainer<DataType>::const_iterator begin =
      mDataContainer->constBegin();
  typename BaseChartDataContainer<DataType>::const_iterator end =
      mDataContainer->constEnd();
  if (DataType::sortKeyIsMainKey())  // we can assume that data is sorted by
                                     // main key, so can reduce the searched key
                                     // interval:
  {
    // determine which key range comes into question, taking selection tolerance
    // around pos into account:
    double posKeyMin, posKeyMax, dummy;
    pixelsToCoords(pos - QPointF(mParentPlot->selectionTolerance(),
                                 mParentPlot->selectionTolerance()),
                   posKeyMin, dummy);
    pixelsToCoords(pos + QPointF(mParentPlot->selectionTolerance(),
                                 mParentPlot->selectionTolerance()),
                   posKeyMax, dummy);
    if (posKeyMin > posKeyMax) qSwap(posKeyMin, posKeyMax);
    begin = mDataContainer->findBegin(posKeyMin, true);
    end = mDataContainer->findEnd(posKeyMax, true);
  }
  if (begin == end) return -1;
  BaseChartRange keyRange(mKeyAxis->range());
  BaseChartRange valueRange(mValueAxis->range());
  for (typename BaseChartDataContainer<DataType>::const_iterator it = begin;
       it != end; ++it) {
    const double mainKey = it->mainKey();
    const double mainValue = it->mainValue();
    if (keyRange.contains(mainKey) &&
        valueRange.contains(
            mainValue))  // make sure data point is inside visible range, for
                         // speedup in cases where sort key isn't main key and
                         // we iterate over all points
    {
      const double currentDistSqr =
          BaseChartVector2D(coordsToPixels(mainKey, mainValue) - pos)
              .lengthSquared();
      if (currentDistSqr < minDistSqr) {
        minDistSqr = currentDistSqr;
        minDistIndex = int(it - mDataContainer->constBegin());
      }
    }
  }
  if (minDistIndex != mDataContainer->size())
    selectionResult.addDataRange(
        BaseChartDataRange(minDistIndex, minDistIndex + 1), false);

  selectionResult.simplify();
  if (details) details->setValue(selectionResult);
  return qSqrt(minDistSqr);
}

/*!
  Splits all data into selected and unselected segments and outputs them via \a
  selectedSegments and \a unselectedSegments, respectively.

  This is useful when subclasses implement their \ref draw method and need to
  draw selected segments with a different pen/brush than unselected segments
  (also see \ref QCPSelectionDecorator).

  \see setSelection
*/
template <class DataType>
void BaseChartAbstractPlottable1D<DataType>::getDataSegments(
    QList<BaseChartDataRange>& selectedSegments,
    QList<BaseChartDataRange>& unselectedSegments) const {
  selectedSegments.clear();
  unselectedSegments.clear();
  if (mSelectable ==
      stWhole)  // stWhole selection type draws the entire plottable with
                // selected style if mSelection isn't empty
  {
    if (selected())
      selectedSegments << BaseChartDataRange(0, dataCount());
    else
      unselectedSegments << BaseChartDataRange(0, dataCount());
  } else {
    BaseChartDataSelection sel(selection());
    sel.simplify();
    selectedSegments = sel.dataRanges();
    unselectedSegments =
        sel.inverse(BaseChartDataRange(0, dataCount())).dataRanges();
  }
}

/*!
  A helper method which draws a line with the passed \a painter, according to
  the pixel data in \a lineData. NaN points create gaps in the line, as expected
  from QCustomPlot's plottables (this is the main difference to QPainter's
  regular drawPolyline, which handles NaNs by lagging or crashing).

  Further it uses a faster line drawing technique based on \ref
  BaseChartPainter::drawLine rather than \c QPainter::drawPolyline if the
  configured \ref QCustomPlot::setPlottingHints() and \a painter style allows.
*/
template <class DataType>
void BaseChartAbstractPlottable1D<DataType>::drawPolyline(
    BaseChartPainter* painter, const QVector<QPointF>& lineData) const {
  // if drawing lines in plot (instead of PDF), reduce 1px lines to cosmetic,
  // because at least in Qt6 drawing of "1px" width lines is much slower even
  // though it has same appearance apart from High-DPI. In High-DPI cases people
  // must set a pen width slightly larger than 1.0 to get correct DPI scaling of
  // width, but of course with performance penalty.
  if (!painter->modes().testFlag(BaseChartPainter::pmVectorized) &&
      qFuzzyCompare(painter->pen().widthF(), 1.0)) {
    QPen newPen = painter->pen();
    newPen.setWidth(0);
    painter->setPen(newPen);
  }

  // if drawing solid line and not in PDF, use much faster line drawing instead
  // of polyline:
  if (mParentPlot->plottingHints().testFlag(phFastPolylines) &&
      painter->pen().style() == Qt::SolidLine &&
      !painter->modes().testFlag(BaseChartPainter::pmVectorized) &&
      !painter->modes().testFlag(BaseChartPainter::pmNoCaching)) {
    int i = 0;
    bool lastIsNan = false;
    const int lineDataSize = lineData.size();
    while (i < lineDataSize &&
           (qIsNaN(lineData.at(i).y()) ||
            qIsNaN(lineData.at(i).x())))  // make sure first point is not NaN
      ++i;
    ++i;  // because drawing works in 1 point retrospect
    while (i < lineDataSize) {
      if (!qIsNaN(lineData.at(i).y()) &&
          !qIsNaN(lineData.at(i).x()))  // NaNs create a gap in the line
      {
        if (!lastIsNan)
          painter->drawLine(lineData.at(i - 1), lineData.at(i));
        else
          lastIsNan = false;
      } else
        lastIsNan = true;
      ++i;
    }
  } else {
    int segmentStart = 0;
    int i = 0;
    const int lineDataSize = lineData.size();
    while (i < lineDataSize) {
      if (qIsNaN(lineData.at(i).y()) || qIsNaN(lineData.at(i).x()) ||
          qIsInf(
              lineData.at(i).y()))  // NaNs create a gap in the line. Also
                                    // filter Infs which make drawPolyline block
      {
        painter->drawPolyline(
            lineData.constData() + segmentStart,
            i - segmentStart);  // i, because we don't want to include the
                                // current NaN point
        segmentStart = i + 1;
      }
      ++i;
    }
    // draw last segment:
    painter->drawPolyline(lineData.constData() + segmentStart,
                          lineDataSize - segmentStart);
  }
}
